/*
 * Copyright 1999-2021 Alibaba Group Holding Ltd.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.alibaba.chaosblade.box.toolsmgr.impl;

import cn.hutool.core.io.IoUtil;
import com.alibaba.chaosblade.box.common.common.domain.response.Response;
import com.alibaba.chaosblade.box.common.common.util.SystemPropertiesUtils;
import com.alibaba.chaosblade.box.toolsmgr.ChaosToolsMgr;
import com.alibaba.chaosblade.box.toolsmgr.annotation.ChannelStrategy;
import com.alibaba.chaosblade.box.toolsmgr.ansible.AnsibleCommand;
import com.alibaba.chaosblade.box.toolsmgr.ansible.command.AnsibleAppendHostCommand;
import com.alibaba.chaosblade.box.toolsmgr.ansible.command.AnsibleListAllHostsCommand;
import com.alibaba.chaosblade.box.toolsmgr.ansible.command.AnsiblePingCommand;
import com.alibaba.chaosblade.box.toolsmgr.ansible.command.AnsibleShellScriptCommand;
import com.alibaba.chaosblade.box.toolsmgr.enums.ChannelType;
import com.alibaba.chaosblade.box.toolsmgr.model.MgrRequest;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

import static com.alibaba.chaosblade.box.toolsmgr.ansible.AnsibleConstants.ANSIBLE_LINUX_DEFAULT_PORT;
import static com.alibaba.chaosblade.box.toolsmgr.ansible.AnsibleConstants.SSH_FILE_COPY_DEFAULT_PATH;

/**
 * @author yefei
 */
@Slf4j
@Component
@ChannelStrategy(ChannelType.ANSIBLE)
public class AnsibleChaosToolsMgr implements ChaosToolsMgr<MgrRequest> {

    private static final long DEFAULT_TIME_OUT = 3000;

    /**
     * 部署并安装 ChaosAgent
     *
     * @param mgrRequest 管理请求参数，包含目标主机信息和安装命令等
     * @return 安装结果响应对象
     */
    @Override
    public Response<String> deployAndInstallAgent(MgrRequest mgrRequest) {
        // 创建 Ansible Shell 脚本命令执行器
        AnsibleCommand ansibleCommand = new AnsibleShellScriptCommand();
        // 执行命令，设置超时时间为3分钟
        return ansibleExecutor(ansibleCommand.getCommand(mgrRequest), ansibleCommand, 3 * 60000, TimeUnit.MICROSECONDS);
    }

    /**
     * 卸载 ChaosAgent
     *
     * @param mgrRequest 管理请求参数，包含目标主机信息
     * @return 卸载结果响应对象
     */
    @Override
    public Response<String> uninstallAgent(MgrRequest mgrRequest) {
        // 设置卸载命令
        String command = String.format("sh /opt/chaos/chaosctl.sh uninstall");
        mgrRequest.setCommand(command);
        // 创建 Ansible Shell 脚本命令执行器
        AnsibleCommand ansibleCommand = new AnsibleShellScriptCommand();
        // 执行卸载命令
        return ansibleExecutor(ansibleCommand.getCommand(mgrRequest), ansibleCommand);
    }

    /**
     * 测试与目标主机的连接
     *
     * @param mgrRequest 管理请求参数，包含目标主机信息
     * @return 连接测试结果响应对象
     */
    @Override
    public Response<String> testConnection(MgrRequest mgrRequest) {
        log.info("[ansible testConnection] Testing connection to host: {}", mgrRequest.getInstanceIp());
        // 检查本地主机列表并添加目标主机（如果不存在）
        if (checkLocalHostAndAppend(mgrRequest)) {
            // 创建 Ansible Ping 命令执行器
            AnsibleCommand ansiblePingCommand = new AnsiblePingCommand();
            // 执行 ping 命令测试连接
            Response<String> pingResult1 = ansibleExecutor(ansiblePingCommand.getCommand(mgrRequest), ansiblePingCommand);
            if (pingResult1.isSuccess()) {
                log.info("[ansible testConnection] Connection test successful for host: {}", mgrRequest.getInstanceIp());
                return pingResult1;
            }

            // 如果 ping 失败，则尝试发送 SSH 密钥进行认证
            log.info("[ansible testConnection] Ping failed, attempting to send SSH key for host: {}", mgrRequest.getInstanceIp());
            Response<String> sendResult = sendSSHKeyIfNotExitOnPassword(mgrRequest);
            if (!sendResult.isSuccess()) {
                String errorMsg = "[ansible testConnection] Failed to send SSH key: " + sendResult.getError();
                log.error(errorMsg);
                return sendResult;
            }

            // 再次执行 ping 命令测试连接
            log.info("[ansible testConnection] SSH key sent, retrying ping for host: {}", mgrRequest.getInstanceIp());
            Response<String> finalResult = ansibleExecutor(ansiblePingCommand.getCommand(mgrRequest), ansiblePingCommand);
            if (finalResult.isSuccess()) {
                log.info("[ansible testConnection] Connection test successful after SSH key exchange for host: {}", mgrRequest.getInstanceIp());
            } else {
                log.error("[ansible testConnection] Connection test failed after SSH key exchange for host: {}", mgrRequest.getInstanceIp());
            }
            return finalResult;
        }
        // 如果无法添加到主机列表，则返回错误
        String errorMsg = "Cannot append to /etc/ansible/hosts";
        log.error("[ansible testConnection] {}", errorMsg);
        return Response.ofFailure(Response.Code.INVALID_Parameter, errorMsg);
    }

    /**
     * Ping 目标主机以检查连接状态
     *
     * @param mgrRequest 管理请求参数，包含目标主机信息
     * @return Ping 结果响应对象
     */
    @Override
    public Response<String> pingConnection(MgrRequest mgrRequest) {
        // 创建 Ansible Ping 命令执行器
        AnsibleCommand ansiblePingCommand = new AnsiblePingCommand();
        // 执行 ping 命令
        return ansibleExecutor(ansiblePingCommand.getCommand(mgrRequest), ansiblePingCommand);
    }

    /**
     * 发送 SSH Key 授权文件到目标主机
     *
     * @param mgrRequest 管理请求参数，包含目标主机信息和密码
     * @return 发送结果响应对象
     */
    private static Response<String> sendSSHKeyIfNotExitOnPassword(MgrRequest mgrRequest) {
        log.info("[ansible] send ssh key, starting...");
        // 检查是否提供了 SSH 密码
        if (StringUtils.isBlank(mgrRequest.getInstancePassword())) {
            String errorMsg = "SSH password must be input";
            log.warn("[ansible sendSSHKey] {}", errorMsg);
            return Response.ofFailure(Response.Code.INVALID_Parameter, errorMsg);
        }
        // 获取端口号，默认为 22
        String port = mgrRequest.getInstancePort() == null ? ANSIBLE_LINUX_DEFAULT_PORT.toString()
                : mgrRequest.getInstancePort();

        // 构造 expect 脚本命令
        String[] cmds = new String[]{"/usr/bin/expect", SSH_FILE_COPY_DEFAULT_PATH, mgrRequest.getInstanceIp(),
                mgrRequest.getInstanceUser(), mgrRequest.getInstancePassword(), port};

        try {
            log.info("[ansible sendSSHKey] Executing expect script for host: {}", mgrRequest.getInstanceIp());
            // 执行 expect 脚本
            Process process = Runtime.getRuntime().exec(cmds, null, null);
            process.waitFor(DEFAULT_TIME_OUT, TimeUnit.MICROSECONDS);
            // 检查是否有错误输出
            if (process.getErrorStream().available() > 0) {
                String error = IoUtil.read(process.getErrorStream(), System.getProperty("file.encoding"));
                log.error("[ansible sendSSHKey] Failed : {}", error);
                return Response.ofFailure(Response.Code.INVALID_Parameter, error);
            } else {
                // 读取标准输出内容
                List<String> content = new ArrayList<>(16);
                IoUtil.readLines(new InputStreamReader(process.getInputStream()), content);
                String contentJson = JSON.toJSONString(content);
                log.info("[ansible sendSSHKey] Content: {}", contentJson);

                String result = IoUtil.read(process.getInputStream(), System.getProperty("file.encoding"));
                log.info("[ansible sendSSHKey] Result: {}", result);
                // 判断是否成功发送密钥
                if (result.contains("rc=0") || result.contains("Number of key(s) added: 1")) {
                    log.info("[ansible sendSSHKey] SSH key successfully sent to host: {}", mgrRequest.getInstanceIp());
                    return Response.ofSuccess("");
                } else {
                    log.warn("[ansible sendSSHKey] Failed to send SSH key to host: {}", mgrRequest.getInstanceIp());
                    return Response.ofFailure(Response.Code.INVALID_Parameter, result);
                }
            }
        } catch (Exception e) {
            // 捕获异常并记录日志
            log.error("[ansible sendSSHKey] Exception " + mgrRequest + e.getMessage(), e);
            return Response.ofFailure(Response.Code.INVALID_Parameter, e.getMessage());
        }
    }

    /**
     * 检查本地主机并追加到Ansible hosts文件
     *
     * @param mgrRequest 管理请求对象
     * @return 操作是否成功
     */
    private static boolean checkLocalHostAndAppend(MgrRequest mgrRequest) {
        // 检查ansible是否安装
        if (!isAnsibleInstalled()) {
            log.warn("Ansible is not installed, skip appending to hosts file");
            return false;
        }
        
        // 创建Ansible追加主机命令
        AnsibleCommand ansibleAppendHostCommand = new AnsibleAppendHostCommand();
        // 执行追加命令
        Response<String> returns = ansibleExecutor(ansibleAppendHostCommand.getCommand(mgrRequest), ansibleAppendHostCommand);
        return returns.isSuccess();
    }

    /**
     * 检查Ansible是否已安装
     *
     * @return Ansible是否已安装
     */
    private static boolean isAnsibleInstalled() {
        try {
            Process checkProcess = Runtime.getRuntime().exec(new String[]{"which", "ansible"});
            boolean checkFinished = checkProcess.waitFor(5, TimeUnit.SECONDS);
            
            // 如果检查进程未完成，销毁它
            if (!checkFinished) {
                checkProcess.destroyForcibly();
                return false;
            }
            
            return checkProcess.exitValue() == 0;
        } catch (Exception e) {
            log.warn("Failed to check if ansible is installed: {}", e.getMessage());
            return false;
        }
    }

    /**
     * 执行 Ansible 命令的通用方法（默认超时）
     *
     * @param ansibleCommand  要执行的 Ansible 命令字符串
     * @param ansibleCommandd Ansible 命令接口实现类
     * @return 执行结果响应对象
     */
    private static Response<String> ansibleExecutor(String ansibleCommand, AnsibleCommand ansibleCommandd) {
        return ansibleExecutor(ansibleCommand, ansibleCommandd, DEFAULT_TIME_OUT, TimeUnit.MICROSECONDS);
    }

    /**
     * 执行 Ansible 命令的通用方法（自定义超时）
     *
     * @param ansibleCommand  要执行的 Ansible 命令字符串
     * @param ansibleCommandd Ansible 命令接口实现类
     * @param timeout         超时时间
     * @param unit            超时时间单位
     * @return 执行结果响应对象
     */
    private static Response<String> ansibleExecutor(String ansibleCommand, AnsibleCommand ansibleCommandd,
                                                    long timeout, TimeUnit unit) {
        try {
            // 检查ansible是否已安装
            Process checkProcess = Runtime.getRuntime().exec(new String[]{"which", "ansible"});
            boolean checkFinished = checkProcess.waitFor(5, TimeUnit.SECONDS);
            
            // 如果检查进程未完成，销毁它
            if (!checkFinished) {
                checkProcess.destroyForcibly();
            } else if (checkProcess.exitValue() != 0) {
                // ansible未安装
                String errorMsg = "Ansible is not installed. Please install Ansible before proceeding. " +
                    "For Ubuntu/Debian: sudo apt-get install ansible. " +
                    "For CentOS/RHEL: sudo yum install ansible. " +
                    "For macOS: brew install ansible.";
                log.warn("[ansible check] {}", errorMsg);
                return Response.ofFailure(Response.Code.Deploy_Error, errorMsg);
            }

            // 执行 Ansible 命令
            log.info("[ansible executor] Executing command: {}", ansibleCommand);
            Process process = Runtime.getRuntime().exec(new String[]{
                    "/bin/sh", "-c", ansibleCommand}, null, null);
            boolean finished = process.waitFor(timeout, unit);
            
            // 如果进程未在指定时间内完成，则销毁进程
            if (!finished) {
                process.destroyForcibly();
                String timeoutMsg = "Command execution timeout: " + ansibleCommand;
                log.warn("[ansible executor] {}", timeoutMsg);
                return Response.ofFailure(Response.Code.Deploy_Error, timeoutMsg);
            }

            // 读取错误输出
            String errorOutput = "";
            try (InputStreamReader errorReader = new InputStreamReader(process.getErrorStream())) {
                errorOutput = IoUtil.read(errorReader);
            }

            // 读取标准输出
            String standardOutput = "";
            try (InputStreamReader inputReader = new InputStreamReader(process.getInputStream())) {
                standardOutput = IoUtil.read(inputReader);
            }

            // 记录输出日志
            if (!errorOutput.isEmpty()) {
                log.info("[ansible executor] Error output for command '{}': {}", ansibleCommand, errorOutput);
            }
            if (!standardOutput.isEmpty()) {
                log.info("[ansible executor] Standard output for command '{}': {}", ansibleCommand, standardOutput);
            }

            // 检查是否有错误输出
            if (!errorOutput.isEmpty()) {
                log.warn("[ansible executor] failed, command: {}, err: {}", ansibleCommand, errorOutput);
                return Response.ofFailure(Response.Code.Deploy_Error, errorOutput);
            } else {
                // 将标准输出解析为行列表
                List<String> content = new ArrayList<>();
                if (!standardOutput.isEmpty()) {
                    try (BufferedReader reader = new BufferedReader(new StringReader(standardOutput))) {
                        String line;
                        while ((line = reader.readLine()) != null) {
                            content.add(line);
                        }
                    }
                }

                // 使用命令实现类判断执行结果是否成功
                if (!ansibleCommandd.resultPredicate(content, true)) {
                    String errorMsg = JSON.toJSONString(content);
                    log.warn("[ansible executor] failed, command: {}, err: {}", ansibleCommand, errorMsg);
                    return Response.ofFailure(Response.Code.Deploy_Error, errorMsg);
                }
                log.info("[ansible executor] success! command: {}", ansibleCommand);
                return Response.ofSuccess(JSON.toJSONString(content));
            }
        } catch (Exception e) {
            // 捕获异常并记录日志
            log.error("[ansible executor] Exception occurred while executing command: " + ansibleCommand, e);
            return Response.ofFailure(Response.Code.Deploy_Error, e.getMessage());
        }
    }
}
